#!/usr/bin/python

import cgi
import csv
import datetime
import re
import sys
import traceback
import StringIO

form = cgi.FieldStorage();

#datafile = "/usr/local/rattus.net/data/aussie-trade-jan-2012.txt"
datafile = "/usr/local/rattus.net/data/aussie-trade-jan-2013.txt"
logfile = "/usr/local/rattus.net/data/tradelogs.txt"

# these no longer work, although we can generate them automatically
# from the data.
# maxcode = 690

print "Content-type: text/html"
print

# logic for handling the game data

def ReadCodes(filename):
  "Read the complete list of codes in from a datafile and sort them"

  # all codes here.
  codes = {}
  # a dict mapping all valid numbers to their associated name string.
  numberlist = {}
  # a dict mapping all valid names to the list of numbers that have them.
  namelist = {}
  # a dict mapping each BGG user to a set of all the codes they
  # own, so we can check that people only do their own codes, and
  # don't miss any.
  user_codes = {}

  fh = open(filename, "r")
  csvfh = csv.reader(fh)
  headerline = csvfh.next()

  for row in csvfh:
    rowd = dict(zip(headerline, row))

    code = rowd["mtcode"]
    codes[code] = rowd["objectname"]
    bits = code.split("-")
    numberlist[bits[0]] = bits[1]

    # since we can have duplicate names, we'll map names to a list of
    # the numbers sporting them.
    if bits[1] not in namelist:
      namelist[bits[1]] = []
    namelist[bits[1]].append(bits[0])

    username = rowd["username"]
    if username not in user_codes:
      user_codes[username] = []
    user_codes[username].append(code)

  # convert this to a set.
  for user in user_codes:
    user_codes[user] = set(user_codes[user])

  return { "codes" : codes,
           "bynum" : numberlist,
           "byname" : namelist,
           "byuser" : user_codes,
           "mincode" : min(map(int,numberlist.keys())),
           "maxcode" : max(map(int, numberlist.keys())) }

def CheckCode(code, codes):
  "Check whether a supplied code is a valid code for the maths trade"
  # Let's be optimistic first.
  if code in codes["codes"]:
    return True, ""

  if code == "LIMIT":
    return True, "limit"

  if re.match(r"\%[-A-Za-z0-9]+", code):
    return True, "group"

  # We'll special case cash bids here.
  m = re.match(r"\$([0-9]+)$", code)
  if m:
    if int(m.group(1)) > 0:
      return True, "money"
    else:
      return False, "Money amounts must be positive"

  m = re.match(r"^([0-9]+)-([A-Z0-9]+)$", code)
  if not m:
    return False, "Unable to parse code. It must be of the form NNNNNNN-AAAAA %AAAAA or $NN"

  codenumtext = m.group(1)
  codetext = m.group(2)
  codenum = int(codenumtext)
  warning = ""
  if codenum < codes["mincode"] or codenum > codes["maxcode"]:
    warning = "Code number out of range %d-%d\n" % (codes["mincode"],
                                                  codes["maxcode"])
  elif codenumtext not in codes["bynum"]:
    warning += "Code number is %s not valid" % codenumtext + "\n"
  else:
    # code number _was_ found.
    realname = codes["bynum"][codenumtext]
    warning += "Code %s doesn't match name. Did you mean %s?\n" % (
      codenumtext, codenumtext + "-" + realname)

  if codetext not in codes["byname"]:
    warning += "Code name %s is not valid" % codetext + "\n"
  else:
    # code text _was_ found.
    warning += ("Did you mean one of these codes for %s: " % codetext)
    warning += " ".join(codes["byname"][codetext]) + " ?"

  warning = warning.strip("\n")
  return False, warning


def VerifyList(tradelist, codes):
  "Verified each line of the tradelist and generate a report"

  bgguser = None

  errors = { "all" : False, "line": False }
  had_errors = False
  line_count = 0
  money_bids = False
  limit_set = False
  report = StringIO.StringIO()

  groups_defined = set()
  groups_used = set()
  left_items = set()
  right_items = set()

  def AddError(*terms):
    print >>report, "<span class=error>", " ".join(terms), "</span>"
    errors["all"]  = True
    errors["line"] = True

  def AddInfo(*terms):
    print >>report, "<span class=info>", " ".join(terms), "</span>"



  for line in tradelist.split("\r"):
    line = line.strip()
    if not(line):
      print >>report
      continue

    print >>report, cgi.escape(line)
    # comments.
    if re.match("^#", line):
      continue

    # from here, this is some sort of interesting line.
    line_count += 1
    left_money = False
    right_money = False
    errors["line"] = False

    #
    # Check initial name part.
    #

    m = re.match(r"\(([^)]+)\)\s*(.*)", line)
    if not m:
      AddError("Each trade line must begin with a BGG username in ().")
      continue
    if not bgguser:
      bgguser = m.group(1)
    else:
      if m.group(1) != bgguser:
        AddError("BGG username in () doesn't match earlier lines.")
        # nonfatal.

    line = m.group(2)
    bits = line.split(":")
    if len(bits) != 2:
      AddError("Line does not contain exactly one :")
      continue

    left_codes = bits[0].split()
    if len(left_codes) != 1:
      AddError("More than one token to the left of the :")
      continue

    #
    # Check the left hand side.
    #

    left_code = left_codes[0]
    valid, left_message = CheckCode(left_code, codes)
    # TODO, check up on the various other types of code here.
    if not valid:
      AddError("On left side:", cgi.escape(left_code),
               "is invalid.", left_message)

    if not left_message:
      if (bgguser not in codes["byuser"] or
          left_code not in codes["byuser"][bgguser]):
        AddError("Item", cgi.escape(left_code), "was not listed by user",
                 cgi.escape(bgguser), "!")

      if left_code in left_items:
        AddError("Item", cgi.escape(left_code), "has multiple want lists.")
      else:
        left_items.add(left_code)

    if left_message == "money":
      left_money = True

    if left_message == "group":
      if left_code in groups_defined:
        AddError("Duplication protection group", cgi.escape(left_code),
                 "defined more than once!")
      groups_defined.add(left_code)

    if left_message == "limit":
      limit_value = bits[1].split()
      limit_error = False
      if len(limit_value) != 1:
        limit_error = True
      else:
        valid, message = CheckCode(limit_value[0], codes)
        if not valid or message != "money":
          limit_error = True

      if limit_error:
        AddError("A LIMIT must be followed by a dollar value")
        continue

      if limit_set:
        AddError("LIMIT was set more than once")
      limit_set = True


    #
    # Iterate through the right hand side.
    #

    all_right_codes = set()
    right_codes = bits[1].split()
    if len(right_codes) == 0:
      AddInfo("Warning. Empty want list.")

    for code in right_codes:
      safecode = cgi.escape(code)
      if code == left_code:
        AddError("You want game", safecode, "for itself.")
        continue

      valid, message = CheckCode(code, codes)
      if not valid:
        AddError("On right:", safecode, "is invalid.", message)
        continue

      if not message:
        right_items.add(code)

      if not message and code in all_right_codes:
        AddError("Code", safecode, "appears multiple times.")
      else:
        all_right_codes.add(code)

      if message == "money":
        if right_money == True:
          AddError("Multiple dollar amounts on right side.")
        right_money = True

      if message == "group":
        if left_message == "group":
          AddError("A %GROUP was used on the left and right side.")
        groups_used.add(code)

      if message == "limit":
        AddError("You cannot have a LIMIT on the right")

    if left_money and right_money:
      AddError("Money on left and right.")
    if left_money:
      money_bids = True

    if not errors["line"]:
      AddInfo("ok.")

  print >>report

  # we should warn differently if there are multiple money bids, vs if there
  # are just one.
  if money_bids and not limit_set:
    AddInfo("Warning. Money bids made but no LIMIT set!")

  items_listed = set(left_items)
  if items_listed and items_listed != codes["byuser"][bgguser]:
    AddInfo("The user posted no want lists for the following posted items:",
            " ".join(codes["byuser"][bgguser] - items_listed))

  unused_groups = groups_defined - groups_used
  undefined_groups = groups_used - groups_defined
  if unused_groups:
    AddInfo("Warning. Unused groups:",cgi.escape(" ".join(unused_groups)))
  if undefined_groups:
    AddError("Error. Undefined groups were used in wantlists:",
             cgi.escape(" ".join(undefined_groups)))
  left_and_right = left_items.intersection(right_items)
  if left_and_right:
    AddError("Error. Items appear both left and right sides:",
             cgi.escape(" ".join(left_and_right)))


  #  m = re.match("
  if not errors["all"]:
    print >>report
    print >>report, "<b>All lines verified okay. Please submit via geekmail.</b>"
  else:
    print >>report
    print >>report, "<b>Some errors were found. Correct and reverify.</b>"

  return report.getvalue().replace("\n", "<br>\n")

# core functionality here.
if len(sys.argv) > 1:
  tradelist = open(sys.argv[1], "r").read()
else:
  tradelist = form.getfirst("tradelist", "")

if tradelist:

  try:
    log = open(logfile, "a")
    log.write(datetime.datetime.now().isoformat(" ") + "\n")
    log.write(tradelist + "\n\n")
    codes = ReadCodes(datafile)
    verify_message = VerifyList(tradelist, codes)

  except:

    verify_message = "Error in verification service. Please geekmail thepackrat with details of what you tried. We apologise for the inconvenience."

    exc_type, exc_value, exc_traceback = sys.exc_info()
    trace_lines = traceback.format_exception(exc_type, exc_value, exc_traceback)

    log = open(logfile, "a")
    log.write(datetime.datetime.now().isoformat(" ") + "\n")
    log.write("ERROR\n")
    log.write(tradelist + "\n")
    log.write(''.join('!! ' + line for line in trace_lines) + "\n")


else:
  verify_message = "No tradelist entered."

warning_message = """
<font size=+1 color=red><b>
<br> The tool doesn't yet have the final item list for the 2012 year-end aussie
maths trade. Please check back after the codes are finalised.
</b></font>
<br>
"""

warning_message = """
<font size=+1 color=red><b>
<br> The 2012 end of year maths trade tool is just being brought up. You
may experience oddities until I'm done. </b></font>
<br>
"""


warning_message = ""

print """<html>
<head><title>BGG Maths trade list verifier</title>
<style type="text/css">
.error {
  font-weight: bold;
  color: #ff0000;
}
.info {
  font-weight: bold;
  color: #000000;
}
</style>
</head>
<body>

<h1> BGG Aussie maths-trade verifier (EOY 2012) </h1>


<div> <P> The purpose of this tool is to give quick feedback on some of the
simple errors that can creep into a tradelist. Once your tradelist
verifies okay here, submit it via geekmail as normal. Report issues
via geekmail to thepackrat.
</div>

<div class="warning">
%s
</div>

<div class="report">
<tt>
%s
</tt>
<div>

<br><br>

<form method="post" action="checktrades.py">
Paste a copy of your trade list below:<br>
<textarea cols=80 rows=10 name="tradelist">
%s</textarea><br>
<input type=submit value="verify">
</form> 

</body>

""" % (warning_message, verify_message, cgi.escape(tradelist))
